package per.chao.mqtt.infrastructure.exception;

import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * Description: 参数格式化
 *
 * @author W.Chao
 * @date 2020/11/7 15:13
 **/
public final class ParameterFormatUtil {
	/**
	 * Prefix for recursion.
	 */
	private static final String RECURSION_PREFIX = "[...";
	/**
	 * Suffix for recursion.
	 */
	private static final String RECURSION_SUFFIX = "...]";

	/**
	 * Prefix for errors.
	 */
	private static final String ERROR_PREFIX = "[!!!";
	/**
	 * Separator for errors.
	 */
	private static final String ERROR_SEPARATOR = "=>";
	/**
	 * Separator for error messages.
	 */
	private static final String ERROR_MSG_SEPARATOR = ":";
	/**
	 * Suffix for errors.
	 */
	private static final String ERROR_SUFFIX = "!!!]";

	private static final char BRACES_START = '{';
	private static final char BRACES_STOP = '}';
	private static final char ESCAPE_CHAR = '\\';

	private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

	private ParameterFormatUtil() {
	}

	/**
	 * 计算占位符{}数量
	 *
	 * @param messagePattern the message pattern to be analyzed.
	 * @return the number of unescaped placeholders.
	 */
	public static int countArgumentPlaceholders(final String messagePattern) {
		if (messagePattern == null) {
			return 0;
		}
		final int length = messagePattern.length();
		int result = 0;
		boolean isEscaped = false;
		for (int i = 0; i < length - 1; i++) {
			final char curChar = messagePattern.charAt(i);
			if (curChar == ESCAPE_CHAR) {
				isEscaped = !isEscaped;
			} else if (curChar == BRACES_START) {
				if (!isEscaped && messagePattern.charAt(i + 1) == BRACES_STOP) {
					result++;
					i++;
				}
				isEscaped = false;
			} else {
				isEscaped = false;
			}
		}
		return result;
	}

	/**
	 * 计算未转义占位符{}的数量 -> \{
	 *
	 * @param messagePattern the message pattern to be analyzed.
	 * @return the number of unescaped placeholders.
	 */
	public static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
		if (messagePattern == null) {
			return 0;
		}
		final int length = messagePattern.length();
		int result = 0;
		boolean isEscaped = false;
		for (int i = 0; i < length - 1; i++) {
			final char curChar = messagePattern.charAt(i);
			if (curChar == ESCAPE_CHAR) {
				isEscaped = !isEscaped;
				indices[0] = -1; // escaping means fast path is not available...
				result++;
			} else if (curChar == BRACES_START) {
				if (!isEscaped && messagePattern.charAt(i + 1) == BRACES_STOP) {
					indices[result] = i;
					result++;
					i++;
				}
				isEscaped = false;
			} else {
				isEscaped = false;
			}
		}
		return result;
	}

	/**
	 * Counts the number of unescaped placeholders in the given messagePattern.
	 *
	 * @param messagePattern the message pattern to be analyzed.
	 * @return the number of unescaped placeholders.
	 */
	public static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
		int result = 0;
		boolean isEscaped = false;
		for (int i = 0; i < length - 1; i++) {
			final char curChar = messagePattern[i];
			if (curChar == ESCAPE_CHAR) {
				isEscaped = !isEscaped;
			} else if (curChar == BRACES_START) {
				if (!isEscaped && messagePattern[i + 1] == BRACES_STOP) {
					indices[result] = i;
					result++;
					i++;
				}
				isEscaped = false;
			} else {
				isEscaped = false;
			}
		}
		return result;
	}

	/**
	 * 使用参数数组中的元素替换占位符
	 *
	 * @param messagePattern the message pattern containing placeholders.
	 * @param arguments      the arguments to be used to replace placeholders.
	 * @return the formatted message.
	 */
	public static String format(final String messagePattern, final Object[] arguments) {
		final StringBuilder result = new StringBuilder();
		final int argCount = arguments == null ? 0 : arguments.length;
		formatMessage(result, messagePattern, arguments, argCount);
		return result.toString();
	}

	/**
	 * 使用参数替换占位符
	 *
	 * @param buffer         the buffer to write the formatted message into
	 * @param messagePattern the message pattern containing placeholders.
	 * @param arguments      the arguments to be used to replace placeholders.
	 */
	public static void formatMessage2(final StringBuilder buffer, final String messagePattern,
							   final Object[] arguments, final int argCount, final int[] indices) {
		if (messagePattern == null || arguments == null || argCount == 0) {
			buffer.append(messagePattern);
			return;
		}
		int previous = 0;
		for (int i = 0; i < argCount; i++) {
			buffer.append(messagePattern, previous, indices[i]);
			previous = indices[i] + 2;
			recursiveDeepToString(arguments[i], buffer, null);
		}
		buffer.append(messagePattern, previous, messagePattern.length());
	}

	/**
	 * Replace placeholders in the given messagePattern with arguments.
	 *
	 * @param buffer         the buffer to write the formatted message into
	 * @param messagePattern the message pattern containing placeholders.
	 * @param arguments      the arguments to be used to replace placeholders.
	 */
	public static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
							   final Object[] arguments, final int argCount, final int[] indices) {
		if (messagePattern == null) {
			return;
		}
		if (arguments == null || argCount == 0) {
			buffer.append(messagePattern);
			return;
		}
		int previous = 0;
		for (int i = 0; i < argCount; i++) {
			buffer.append(messagePattern, previous, indices[i]);
			previous = indices[i] + 2;
			recursiveDeepToString(arguments[i], buffer, null);
		}
		buffer.append(messagePattern, previous, patternLength);
	}

	/**
	 * 使用参数替换占位符
	 *
	 * @param buffer         the buffer to write the formatted message into
	 * @param messagePattern the message pattern containing placeholders.
	 * @param arguments      the arguments to be used to replace placeholders.
	 */
	public static void formatMessage(final StringBuilder buffer, final String messagePattern,
							  final Object[] arguments, final int argCount) {
		if (messagePattern == null || arguments == null || argCount == 0) {
			buffer.append(messagePattern);
			return;
		}
		int escapeCounter = 0;
		int currentArgument = 0;
		int i = 0;
		final int len = messagePattern.length();
		for (; i < len - 1; i++) { // last char is excluded from the loop
			final char curChar = messagePattern.charAt(i);
			if (curChar == ESCAPE_CHAR) {
				escapeCounter++;
			} else {
				if (isBracesPair(curChar, messagePattern, i)) { // 前后是否组成占位符
					i++;

					// write escaped escape chars
					writeEscapedEscapeChars(escapeCounter, buffer);

					if (isOdd(escapeCounter)) {
						// i.e. escaped: write escaped escape chars
						writeBracesPair(buffer);
					} else {
						// unescaped
						writeArgOrBracesPair(arguments, argCount, currentArgument, buffer);
						currentArgument++;
					}
				} else {
					handleLiteralChar(buffer, escapeCounter, curChar);
				}
				escapeCounter = 0;
			}
		}
		handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
	}

	/**
	 * 如果当前字符和后一个字符组合为占位符{} {@code curCharIndex + 1} 返回 {@code true}
	 * 否则返回 {@code false}
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static boolean isBracesPair(final char curChar, final String messagePattern, final int curCharIndex) {
		return curChar == BRACES_START && messagePattern.charAt(curCharIndex + 1) == BRACES_STOP;
	}

	/**
	 * Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes
	 * it if necessary, returning the resulting position in the result char array.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void handleRemainingCharIfAny(final String messagePattern, final int len,
												 final StringBuilder buffer, final int escapeCounter, final int i) {
		if (i == len - 1) {
			final char curChar = messagePattern.charAt(i);
			handleLastChar(buffer, escapeCounter, curChar);
		}
	}

	/**
	 * Processes the last unprocessed character and returns the resulting position in the result char array.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
		if (curChar == ESCAPE_CHAR) {
			writeUnescapedEscapeChars(escapeCounter + 1, buffer);
		} else {
			handleLiteralChar(buffer, escapeCounter, curChar);
		}
	}

	/**
	 * 写入除了'\'或'{}'组合的字符
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
		// any other char beside ESCAPE or BRACES_START/STOP-combo
		// write unescaped escape chars
		writeUnescapedEscapeChars(escapeCounter, buffer);
		buffer.append(curChar);
	}

	/**
	 * Writes "{}" to the specified result array at the specified position and returns the resulting position.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void writeBracesPair(final StringBuilder buffer) {
		buffer.append(BRACES_START);
		buffer.append(BRACES_STOP);
	}

	/**
	 * Returns {@code true} if the specified parameter is odd.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static boolean isOdd(final int number) {
		return (number & 1) == 1;
	}

	/**
	 * Writes a '\' char to the specified result array (starting at the specified position) for each <em>pair</em> of
	 * '\' escape chars encountered in the message format and returns the resulting position.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
		final int escapedEscapes = escapeCounter >> 1; // divide by two
		writeUnescapedEscapeChars(escapedEscapes, buffer);
	}

	/**
	 * 将多个'\'字符写入buffer
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
		while (escapeCounter > 0) {
			buffer.append(ESCAPE_CHAR);
			escapeCounter--;
		}
	}

	/**
	 * Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to
	 * the specified result char array at the specified position and returns the resulting position.
	 */
	// Profiling showed this method is important to log4j performance. Modify with care!
	// 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
	private static void writeArgOrBracesPair(final Object[] arguments, final int argCount, final int currentArgument,
											final StringBuilder buffer) {
		if (currentArgument < argCount) {
			recursiveDeepToString(arguments[currentArgument], buffer, null);
		} else {
			writeBracesPair(buffer);
		}
	}

	/**
	 * This method performs a deep toString of the given Object.
	 * Primitive arrays are converted using their respective Arrays.toString methods while
	 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
	 * contain themselves.
	 * <p>
	 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
	 * behavior. They only check if the container is directly contained in itself, but not if a contained container
	 * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
	 * Confusing? Just read the last paragraph again and check the respective toString() implementation.
	 * </p>
	 * <p>
	 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
	 * would produce a relatively hard-to-debug StackOverflowError.
	 * </p>
	 *
	 * @param o The object.
	 * @return The String representation.
	 */
	public static String deepToString(final Object o) {
		if (o == null) {
			return null;
		}
		if (o instanceof String) {
			return (String) o;
		}
		final StringBuilder str = new StringBuilder();
		final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;)
		recursiveDeepToString(o, str, dejaVu);
		return str.toString();
	}

	/**
	 * This method performs a deep toString of the given Object.
	 * Primitive arrays are converted using their respective Arrays.toString methods while
	 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
	 * contain themselves.
	 * <p>
	 * dejaVu is used in case of those container types to prevent an endless recursion.
	 * </p>
	 * <p>
	 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
	 * behavior.
	 * They only check if the container is directly contained in itself, but not if a contained container contains the
	 * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
	 * Confusing? Just read the last paragraph again and check the respective toString() implementation.
	 * </p>
	 * <p>
	 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
	 * would produce a relatively hard-to-debug StackOverflowError.
	 * </p>
	 *
	 * @param o      the Object to convert into a String
	 * @param str    the StringBuilder that o will be appended to
	 * @param dejaVu a list of container identities that were already used.
	 */
	private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
		if (appendSpecialTypes(o, str)) {
			return;
		}
		if (isMaybeRecursive(o)) {
			appendPotentiallyRecursiveValue(o, str, dejaVu);
		} else {
			tryObjectToString(o, str);
		}
	}

	private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
		if (o == null || o instanceof String) {
			str.append((String) o);
			return true;
		} else if (o instanceof CharSequence) {
			str.append((CharSequence) o);
			return true;
		} else if (o instanceof Integer) { // LOG4J2-1415 unbox auto-boxed primitives to avoid calling toString()
			str.append(((Integer) o).intValue());
			return true;
		} else if (o instanceof Long) {
			str.append(((Long) o).longValue());
			return true;
		} else if (o instanceof Double) {
			str.append(((Double) o).doubleValue());
			return true;
		} else if (o instanceof Boolean) {
			str.append(((Boolean) o).booleanValue());
			return true;
		} else if (o instanceof Character) {
			str.append(((Character) o).charValue());
			return true;
		} else if (o instanceof Short) {
			str.append(((Short) o).shortValue());
			return true;
		} else if (o instanceof Float) {
			str.append(((Float) o).floatValue());
			return true;
		}
		return appendDate(o, str);
	}

	private static boolean appendDate(final Object o, final StringBuilder str) {
		if (!(o instanceof Date)) {
			return false;
		}
		final Date date = (Date) o;
		str.append(dateTimeFormatter.format(date.toInstant().atZone(ZoneId.systemDefault())));
		return true;
	}

	/**
	 * Returns {@code true} if the specified object is an array, a Map or a Collection.
	 */
	private static boolean isMaybeRecursive(final Object o) {
		return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
	}

	private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
														final Set<String> dejaVu) {
		final Class<?> oClass = o.getClass();
		if (oClass.isArray()) {
			appendArray(o, str, dejaVu, oClass);
		} else if (o instanceof Map) {
			appendMap(o, str, dejaVu);
		} else if (o instanceof Collection) {
			appendCollection(o, str, dejaVu);
		}
	}

	private static void appendArray(final Object o, final StringBuilder str, Set<String> dejaVu,
									final Class<?> oClass) {
		if (oClass == byte[].class) {
			str.append(Arrays.toString((byte[]) o));
		} else if (oClass == short[].class) {
			str.append(Arrays.toString((short[]) o));
		} else if (oClass == int[].class) {
			str.append(Arrays.toString((int[]) o));
		} else if (oClass == long[].class) {
			str.append(Arrays.toString((long[]) o));
		} else if (oClass == float[].class) {
			str.append(Arrays.toString((float[]) o));
		} else if (oClass == double[].class) {
			str.append(Arrays.toString((double[]) o));
		} else if (oClass == boolean[].class) {
			str.append(Arrays.toString((boolean[]) o));
		} else if (oClass == char[].class) {
			str.append(Arrays.toString((char[]) o));
		} else {
			if (dejaVu == null) {
				dejaVu = new HashSet<>();
			}
			// special handling of container Object[]
			final String id = identityToString(o);
			if (dejaVu.contains(id)) {
				str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
			} else {
				dejaVu.add(id);
				final Object[] oArray = (Object[]) o;
				str.append('[');
				boolean first = true;
				for (final Object current : oArray) {
					if (first) {
						first = false;
					} else {
						str.append(", ");
					}
					recursiveDeepToString(current, str, new HashSet<>(dejaVu));
				}
				str.append(']');
			}
			//str.append(Arrays.deepToString((Object[]) o));
		}
	}

	private static void appendMap(final Object o, final StringBuilder str, Set<String> dejaVu) {
		// special handling of container Map
		if (dejaVu == null) {
			dejaVu = new HashSet<>();
		}
		final String id = identityToString(o);
		if (dejaVu.contains(id)) {
			str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
		} else {
			dejaVu.add(id);
			final Map<?, ?> oMap = (Map<?, ?>) o;
			str.append('{');
			boolean isFirst = true;
			for (final Object o1 : oMap.entrySet()) {
				final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
				if (isFirst) {
					isFirst = false;
				} else {
					str.append(", ");
				}
				final Object key = current.getKey();
				final Object value = current.getValue();
				recursiveDeepToString(key, str, new HashSet<>(dejaVu));
				str.append('=');
				recursiveDeepToString(value, str, new HashSet<>(dejaVu));
			}
			str.append('}');
		}
	}

	private static void appendCollection(final Object o, final StringBuilder str, Set<String> dejaVu) {
		// special handling of container Collection
		if (dejaVu == null) {
			dejaVu = new HashSet<>();
		}
		final String id = identityToString(o);
		if (dejaVu.contains(id)) {
			str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
		} else {
			dejaVu.add(id);
			final Collection<?> oCol = (Collection<?>) o;
			str.append('[');
			boolean isFirst = true;
			for (final Object anOCol : oCol) {
				if (isFirst) {
					isFirst = false;
				} else {
					str.append(", ");
				}
				recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
			}
			str.append(']');
		}
	}

	private static void tryObjectToString(final Object o, final StringBuilder str) {
		// it's just some other Object, we can only use toString().
		try {
			str.append(o.toString());
		} catch (final Throwable t) {
			handleErrorInObjectToString(o, str, t);
		}
	}

	private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
		str.append(ERROR_PREFIX);
		str.append(identityToString(o));
		str.append(ERROR_SEPARATOR);
		final String msg = t.getMessage();
		final String className = t.getClass().getName();
		str.append(className);
		if (!className.equals(msg)) {
			str.append(ERROR_MSG_SEPARATOR);
			str.append(msg);
		}
		str.append(ERROR_SUFFIX);
	}

	/**
	 * This method returns the same as if Object.toString() would not have been
	 * overridden in obj.
	 * <p>
	 * Note that this isn't 100% secure as collisions can always happen with hash codes.
	 * </p>
	 * <p>
	 * Copied from Object.hashCode():
	 * </p>
	 * <blockquote>
	 * As much as is reasonably practical, the hashCode method defined by
	 * class {@code Object} does return distinct integers for distinct
	 * objects. (This is typically implemented by converting the internal
	 * address of the object into an integer, but this implementation
	 * technique is not required by the Java&#8482; programming language.)
	 * </blockquote>
	 *
	 * @param obj the Object that is to be converted into an identity string.
	 * @return the identity string as also defined in Object.toString()
	 */
	public static String identityToString(final Object obj) {
		if (obj == null) {
			return null;
		}
		return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
	}

}
